/*
 * TCSS 558
 * Distributed HashMap
 * Fall 2013
 * James Marquardt
 * Matt Adams
 * Justin Wong
 */
package node;
import static com.google.common.base.Preconditions.*;

import java.rmi.AccessException;
import java.rmi.AlreadyBoundException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import model.Entry;
import model.HostPort;
import model.NodesAndEntries;
import model.RemoteNodeData;
import model.RemoteNodeDataAndEntries;
import node.Node;
import node.NodeActual;
import node.NodeRMIProxy;

/**
 * RemoteNodeRMIEndpoint represents a remote endpoint for each registered
 * and active actual node.
 * 
 * @author Matt Adams
 */
public class RemoteNodeRMIEndpoint implements RemoteNode {
    
    /**
     * Registry name prefix.
     */
    public static final String NAME_IN_REGISTRY = "endpoint:";
    
    /**
     * Map containing all node locations.
     */
    public static final Map<HostPort, RemoteNodeRMIEndpoint> endpoints = new HashMap<HostPort, RemoteNodeRMIEndpoint>();
    
    /**
     * Registry object.
     */
    private Registry registry = null;
    
    /**
     * Remote node. Set when openConnections is called.
     */
    private RemoteNode remoteNode = null;
    
    /**
     * HostPort of node with this RMIEndpoint.
     */
    protected HostPort nodeHostPort;
    
    /**
     * Actual node with this RMIEndpoint.
     */
    protected Node node;
    
    public RemoteNodeRMIEndpoint(final HostPort nodeHostPort, final Node node) {
        this.nodeHostPort = checkNotNull(nodeHostPort, "Node's HostPort cannot be null.");;
        this.node = checkNotNull(node, "Node reference cannot be null.");
    }
    
    /**
     * @return Returns remote node.
     */
    public RemoteNode getRemoteNode() {        
        return checkNotNull(this.remoteNode, "Cannot return null remote node.");
    }
    
    /* (non-Javadoc)
     * @see rmi.RemoteNode#findSuccessor(int)
     */
    @Override
    public RemoteNodeData findSuccessor(int key) throws RemoteException {
        Node node = this.node.findSuccessor(key);
        return nodeToRemoteNodeData(node);

    }

    /* (non-Javadoc)
     * @see rmi.RemoteNode#insertEntry(chord.data.Entry)
     */
    @Override
    public void insertEntry(Entry entry) throws RemoteException {
        this.node.insertEntry(entry);
    }

    /* (non-Javadoc)
     * @see rmi.RemoteNode#removeEntry(chord.data.Entry)
     */
    @Override
    public void removeEntry(Entry entry) throws RemoteException {
        this.node.removeEntry(entry);

    }
    
    @Override
    public Entry getEntry(int key) throws RemoteException {
    	return this.node.getEntry(key);
    }

    /* (non-Javadoc)
     * @see rmi.RemoteNode#containsEntry(chord.data.Entry)
     */
    @Override
    public boolean containsEntry(Entry entry) throws RemoteException {
        // TODO Auto-generated method stub
        return false;
    }

    /* (non-Javadoc)
     * @see rmi.RemoteNode#leaveNetworkNotify(rmi.Node)
     */
    @Override
    public void leaveNetworkNotify(RemoteNodeData predecessor) throws RemoteException {
        this.node.leaveNetworkNotify(new NodeRMIProxy(predecessor, this.nodeHostPort));
    }

    /* (non-Javadoc)
     * @see rmi.RemoteNode#closeConnections()
     */
    @Override
    public void closeConnections() throws RemoteException {
        try {
            UnicastRemoteObject.unexportObject(this, true);
            registry.unbind(NAME_IN_REGISTRY + this.nodeHostPort.toString());
            synchronized (endpoints) {
                endpoints.remove(this.node.nodeHostPort);
            }
            this.node.disconnect();
        } catch (AccessException e) {
        } catch (RemoteException e) {
        } catch (NotBoundException e) {
        }

    }

    /* (non-Javadoc)
     * @see rmi.RemoteNode#openConnections()
     */
    @Override
    public void openConnections() throws RemoteException {
        try {
            if (registry == null) {
                registry = LocateRegistry.createRegistry(this.nodeHostPort.getPort());
            }
            this.remoteNode = (RemoteNode) UnicastRemoteObject.exportObject(this, 0);
            this.registry.bind(NAME_IN_REGISTRY + this.nodeHostPort.toString(), this.remoteNode);
            
        } catch (RemoteException e) {
            throw new RuntimeException(e);
        } catch (AlreadyBoundException e) {
            throw new RuntimeException(e);
        }

    }
    
    @Override
    public RemoteNodeDataAndEntries notifyWithPotentialPredecessor(RemoteNodeData potentialPredecessor)
            throws RemoteException {
        final NodesAndEntries nodesAndEntries= this.node.notifyWithPotentialPredecessor(new NodeRMIProxy(potentialPredecessor, this.nodeHostPort));
        final List<Node> nodes = nodesAndEntries.getNodes();
        final List<RemoteNodeData> remoteData = new ArrayList<RemoteNodeData>();
        for (Node node : nodes) {
            remoteData.add(nodeToRemoteNodeData(node));
        }
        return new RemoteNodeDataAndEntries(remoteData, nodesAndEntries.getEntries());
    }
    
    @Override
    public void setSuccessor(RemoteNodeData successorNode) throws RemoteException {
        this.node.setSuccessor(new NodeRMIProxy(successorNode, this.nodeHostPort));
        System.out.println(this.node.nodeID + " " + successorNode.getNodeID());
        
    }
    
    /**
     * Helper method to convert Node objects to RemoteNodeData objects for RMI communication.
     * 
     * @param node Node to be converted.
     * @return Returns RemoteNodeData object representing the provided Node object.
     * @throws RemoteException RMI Remote Exception.
     */
    private RemoteNodeData nodeToRemoteNodeData(Node node) throws RemoteException {
        checkNotNull(node, "Node used to build remote data cannot be null.");
        checkNotNull(this.node, "This.node in RemoteNodeObject should not be null!");
        if (node.getNodeID() == this.node.getNodeID()) {
            return new RemoteNodeData(this.remoteNode, this.node.getNodeID(), this.node.getNodeHostPort());
        } else {
            return new RemoteNodeData(((NodeRMIProxy) node).getRemoteNode(), node.getNodeID(), node.getNodeHostPort());
        }
    }

    @Override
    public int getNodeID() throws RemoteException {
        return this.node.getNodeID();
    }

    @Override
    public String remoteToString() {
        return ((NodeActual)this.node).getNodeReferences().toString();
    }    
    
    
    public RemoteNodeData getPredecessor() throws RemoteException {
        return nodeToRemoteNodeData(((NodeActual)this.node).getNodeReferences().getPredecessor());
    }
    
    @Override
    public RemoteNodeData getSuccessor() throws RemoteException {
        return nodeToRemoteNodeData(((NodeActual)this.node).getNodeReferences().getSuccessor());
    }
    
    public void addReference(RemoteNodeData node) {
        ((NodeActual)this.node).getNodeReferences().addReference(new NodeRMIProxy(node, node.getHostPort()));
    }
    
    public void addReferenceAndUpdatePredecessor(RemoteNodeData node) {
        ((NodeActual)this.node).getNodeReferences().addReferenceAndUpdatePredecessor(new NodeRMIProxy(node, node.getHostPort()));
    }
    
    public static RemoteNodeRMIEndpoint createRMIEndpoint(Node node, HostPort hostPort) {

        synchronized (endpoints) {
            checkNotNull(hostPort, "HostPort cannot be null.");
            if (endpoints.containsKey(hostPort)) {
                throw new RuntimeException("NodeLocation already created.");
            }
            RemoteNodeRMIEndpoint location = new RemoteNodeRMIEndpoint(hostPort, node);
            endpoints.put(hostPort, location);

            return location;
        }
    }
    public static RemoteNodeRMIEndpoint getRMIEndpoint(HostPort hostPort) {
        synchronized (endpoints) {
            RemoteNodeRMIEndpoint loc = endpoints.get(hostPort);
            return loc;
        }
    }

    @Override
    public HostPort getNodeHostPort() throws RemoteException {
        return this.node.getNodeHostPort();
    }
    
    @Override
    public RemoteNodeDataAndEntries getEntries() {
        final Set<Entry> result = this.node.getEntries();
        return new RemoteNodeDataAndEntries(null, result);
    }

    @Override
    public void addAllEntries(Set<Entry> allEntries) throws RemoteException {
        ((NodeActual)this.node).addAllEntries(allEntries);
        
    }

    @Override
    public void insertCopies(Set<Entry> entryCopiesForInsert) throws RemoteException {
        this.node.insertCopies(entryCopiesForInsert);        
    }

    @Override
    public void removeCopies(Set<Entry> entryCopiesForRemove) throws RemoteException {
        this.node.removeCopies(entryCopiesForRemove);        
    }

    @Override
    public void isLive() throws RemoteException {
        //successful call of this method is proof that node is Live.     
    }



}
